docs(spec): describe the form-section collapse pair on both keys - #19736
Conversation
`FormSectionSchema.collapsible` and `.collapsed` were two published,
authorable booleans with no `.describe()` at all, sitting between
neighbours that have one, so the generated reference page printed two
empty Description cells and the dependency between them reached nobody.
Both now carry contract text, plus one TSDoc block that states the pair
once. Every sentence is measured against the built package: parse does
NOT normalize the pair in either direction (`{ collapsed: true }` parses
to `{ collapsible: false, collapsed: true }`, and `safeParseAsync`
agrees), so the implication `collapsed` -> `collapsible` is a renderer
rule read from the declaration and has to be stated on the declaration
or it reaches no author.
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
…rm-section-collapse-describes
…geset `gen:schema && gen:docs` after the describe edit. Exactly six rows move in `content/docs/references/ui/view.mdx` — the collapse pair in the three places that page projects `FormSectionSchema` — and nothing else under `content/docs/references/**` differs from `origin/main`. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 51c19ce993269aa0d5a41bf842abb3ea28c785ee && git checkout 51c19ce993269aa0d5a41bf842abb3ea28c785ee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e37ea4d0602830b6b0f183b81689b321f127306f 8ea5a49fe145f095658eefa3478a9afa5a26abdf && git checkout -B drift-repro e37ea4d0602830b6b0f183b81689b321f127306f && git merge --no-ff 8ea5a49fe145f095658eefa3478a9afa5a26abdf
node scripts/docs-audit/affected-docs.mjs --json e37ea4d0602830b6b0f183b81689b321f127306f
|
Contract reviewServed-tier: 124/124 Rendered by an isolated at-tier review subagent. ① Derived judgmentsClause-②, path limb: HIT. The diff touches
Every claim in the new describe text, verified at source:
⭐ The drift-check question, settled by PARSING and ⛔ not by name.
⇒ The describe's refusal lives on the accepted construct; the page's tombstone is on the refused one. Likewise ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Tier control — measured by the seat
Two corrections the reviewer made mid-run, recorded rather than smoothed
HousekeepingThe reviewer's Generated by Claude Code |
Fixes #19311
Clause-②: no
Why that reading: two
.describe()strings and one TSDoc block. No key is added, removed or re-typed, no closed set gains a member, no export moves and no refinement changes.check:authorable-surfaceandcheck:api-surfaceare both green with no delta, and the wizard-step andgroupco-declaration refusals parse identically before and after. Grade:patch, measured below rather than pattern-matched.The defect, re-derived on
origin/mainrather than taken on trustFormSectionSchemadeclares two authorable booleans with no.describe()at all, between neighbours that have one:Lit control, same file, same instrument:
view.zod.ts:1054readscollapsed: z.boolean().default(false).describe('Collapse groups by default (presentation only)')— a different schema (group-by presentation). So a describe on a key of this name IS visible to the grep, and the zero at:3315–3316is a reading, not a blind spot. Dark control:git grep -n "collapsedd"over the same file → 0 hits, exit 1.Downstream of that silence, the generated page printed two empty Description cells (
content/docs/references/ui/view.mdx:334-335, and again at:466-467and:482-483, the three places that page projects this schema).This is NOT the surface the first increment landed on. PR #19699 (merged
fa29803417) correctedpackages/spec/src/data/object.zod.ts:1220— theObjectFieldGrouppair, a different schema in a different file, which is why it merged asPart of. Both keys of this pair are described here, not justcollapsed: describing one of a silent pair recreates the ambiguity one key over.Probe — every sentence measured against the BUILT package
Built with
pnpm --filter @objectstack/spec build, then probed through the module theexportsmap resolves (require.resolve('@objectstack/spec/ui')→packages/spec/dist/ui/index.js, printed in the same run).A.
FormSectionSchema.safeParse, one section, all four combinationscollapsibleoutcollapsedoutfalsefalsecollapsible: truetruefalsecollapsible: falsefalsefalsecollapsed: truefalsetruecollapsed: falsefalsefalsetruetruetruecollapsed: true+collapsible: falsefalsetruecollapsed: false+collapsible: truetruefalsefalsefalsefalsecollapsable: true— CONTROL (dark)unrecognized_keyscollapsed: 'true'— CONTROL (dark)invalid_type@collapsedBoth lit and both dark controls answered as predicted, so the nine readings are measurements.
B. The normalizer question the dispatch asked — answered NO
There is no normalizer, no parse wrapper and no fold between these two keys.
{ collapsed: true }parses to{ collapsible: false, collapsed: true }, verbatim, with both keys present in the output (.default(false)), andsafeParseAsyncreturns the same. The section's only.transformisnormalizeVisibleWhen, which touchesvisibleOnand nothing else.⇒ That is the precision limit, and it is stated in the text rather than left implied: the implication
collapsed⇒collapsibleis a renderer rule applied from the declaration, never a parse-time rewrite. The describes say so explicitly, and say that the parsedcollapsiblemust never be read as "a disclosure control renders". This is the exact opposite of theObjectFieldGrouppair, where a real parse-time mapping folds the booleans onto the ADR-0085collapseenum — the two surfaces share key names and share nothing else, so the describes name that difference.C. The two refusals the describes claim
groupcollapsed: truesections.0.collapsedcollapsedcollapsible: truesections.0.collapsiblecollapsiblecollapsed: falsecollapsible: falsesimpleform +collapsed: true— CONTROL (lit)group+fields— CONTROL (dark)groupFormViewSchemais the form CONFIG and carries noname/label/objectName), not a reading — so those rows were discarded and the block re-run after the fixture was repaired. The table above is the repaired run. Recorded because the controls are the only reason the first, wrong version did not ship as a finding.D. Reverse verification — the gate can see this change
After the describe edit and a spec rebuild,
pnpm --filter @objectstack/spec check:generatedexits 1 and names exactly one stale artifact —content/docs/references/**— while the other 14 stay green. Direction observed = turns red, as predicted. Aftergen:schema && gen:docsthe same aggregate exits 0. That is what makes the final green a measurement rather than a silence.Downstream consumer, read at the PINNED sha
Read at
.objectui-sha=87af769e9a3ee28ace099fdd653d3ebd79fe82e2(not objectui's head, which is0cf2d66);git merge-base --is-ancestoron the pin exits 0, which is self-proving.packages/plugin-form/src/ObjectForm.tsx:1560applies the ruling:Boolean(section.collapsible) || Boolean(section.collapsed), with a block comment that states it is read from the DECLARATION and never from live collapse state, and that letters B (refuse the combination) and C (a dev-only warning) were both refused. The describes follow that, and ⛔ neither adds a refinement nor asks for a lint rule.packages/plugin-form/src/DrawerForm.tsx:609and:677still pushcollapsible: section.collapsiblealone on both drawer arms. That file's own comment declares the gap deliberate and fenced, and says converging it is thecollapsed/collapsibledecision. So the contract this PR publishes is the thing that arm was waiting on — it is a sibling-repo residual, another seat's card, and nothing here touches it. Reported, not ridden along.git grepfor a.collapsible/.collapsedmember access acrosspackages/**returns only theObjectFieldGroupmapping and test files;packages/lint/srchas 0 hits forcollapsible, with a positive control (form-section-group-unknown, 2 files) firing on the same command and scope.packages/spec/src/**/*.form.tspaircollapsible: truewithcollapsed: true, so zero in-repo producers exercise the trap. It is an author-facing trap, not a live in-repo defect — which is precisely why the remedy is declaration text.Generated artifacts — the four-step sequence, run mechanically
content/docs/references/**is routedmerge=os-regen, the class that merges with exit 0 and zero conflict markers while silently dropping one side.bash scripts/pm/os-regen-merge.sh— steps 1–3, exit 0. It mergedorigin/main(e37ea4d060), took main's side of the generated artifacts main moved, and committed the merge before any regeneration. ⛔ No rebase, ⛔ no force-push, ⛔ nogit stash, ⛔ no hand merge of a routed path.gen:schemawas run only afterMERGE_HEADwas gone and the tree was clean, so the authorable-surface anchor could not roll back to the old fork point.packages/spec/authorable-surface.base.jsonis byte-identical toorigin/main.pnpm --filter @objectstack/spec build && gen:schema && gen:docs. Exactly one file moved, and by exactly the rows this one change derives: 6 rows incontent/docs/references/ui/view.mdx— the pair, three times, because that page projectsFormSectionSchemathree times.git diff --name-only origin/main -- content/docs/references/names that file and no other.git diff --cached), not the working-tree diff, before committing.Survival assertion for the in-flight sibling, PR #19618
#19618 (open, draft, head
7cc0ca1b3d) touches fourcontent/docs/references/**pages. It is unmerged, so what my regeneration could destroy is main's copy of those pages. Quoted-exactgit grep -F, counts identical on my tree andorigin/main:origin/maintenancytype spelling carryingorganizationField?api/metadata.mdxdata/object.mdxsystem/migration.mdxtenancyspellingSTAMP-ONLY: column carrying the organization a row is ABOUT(its deletion target)data/object.mdxMulti-tenancy configuration for SaaS applicationsAll four of its reference pages are byte-identical to
origin/mainin my tree (git hash-objectvsgit rev-parse origin/main:PATH, four matching pairs). Every os-regen-routed path outsidecontent/docs/references/ui/view.mdxdiffers fromorigin/mainby zero files.Also asserted, because the merge driver swallows implementation bodies and not only index entries: the previous increment's body survives —
git grep -cF "true → 'collapsed' (collapsible, starts closed) on its own"reads 1 inpackages/spec/src/data/object.zod.tsand 2 incontent/docs/references/data/object.mdx, identical on my tree andorigin/main, with a near-miss dark control at 0 / exit 1.Changeset — measured, not pattern-matched
npm pack --dry-run --jsoninpackages/spec(exit 0, 2031 files):src/ui/view.zod.ts— the edited filepackage.json— positive controlsrc/data/object.zod.ts— positive controlsrc/ui/view.test.ts— negative controlscripts/build-docs.ts— negative controltsconfig.json— negative controlScanning the tarball's own file list for the new describe text: 42 shipped files carry it — 30 under
dist/, 11 underjson-schema/, 1 undersrc/. Negative control with a lit leg: the stringare GENERATED — do not hand-edit themis present inpackages/spec/scripts/lib/generated-output.tsand in 0 shipped files, because that file does not ship.⇒ The bytes publish, so
skip-changesetwould be a false declaration. Apatchchangeset is in the diff.Acceptance notes — found, ⛔ not fixed here
packages/spec/src/ui/view.zod.ts:3274and:5951describe the pair in prose and in a doc example without the dependency; neither is falsified by this change and neither is a trap as written (the:5951example writes both keys). Noted, not filed.sections/groupsrows on the generated page print a truncated inline type (… collapsible?: boolean; …) with no Description at all. That is the generator's type-column truncation, not this pair, and it is unchanged here.packages/lintcarries no rule on this pair (0 hits, positive control firing). Consistent with the 2026-09-18 ruling, which refused letter C — a dev-only warning — so this is a deliberate absence, ⛔ not a gap to fill.Generated by Claude Code